# All content Copyright 2010 Cyrus Omar <cyrus.omar@gmail.com> unless otherwise
# specified.
#
# Contributors:
#     Cyrus Omar <cyrus.omar@gmail.com>
#
# This file is part of, and licensed under the terms of, the atomic-hedgehog
# package, except where specified.
#
# The atomic-hedgehog package is free software: you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
#
# The atomic-hedgehog package is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
# License for more details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with the atomic-hedgehog package. If not, see <http://www.gnu.org/licenses/>.

"""Utility functions specific to OpenCL interop."""

import re

import numpy
import pyopencl as cl

import ahh.util as util
import ahh.util.codegen
import ahh.util.np

## Device selection
def get_device((platform_idx, device_idx)):
    """Use with get_device_indices_somehow."""
    return cl.get_platforms()[platform_idx].get_devices()[device_idx]

def get_device_indices_somehow(interactive=True):
    """Returns a pair (platform idx, device idx) either interactively
    (interactive==True) or heuristically.
	"""

    """
	Derived from pyopencl. (pyopencl.create_context_somehow)

	PyOpenCL is licensed to you under the MIT/X Consortium license:

	Copyright (c) 2009 Andreas Klockner and Contributors.

	Permission is hereby granted, free of charge, to any person obtaining a
	copy of this software and associated documentation files (the "Software"),
	to deal in the Software without restriction, including without limitation
	the rights to use, copy, modify, merge, publish, distribute, sublicense,
	and/or sell copies of the Software, and to permit persons to whom the
	Software is furnished to do so, subject to the following conditions:

	The above copyright notice and this permission notice shall be included in
	all copies or substantial portions of the Software.

	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
	OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
	THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
	FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
	DEALINGS IN THE SOFTWARE.
	"""

    try:
        import sys
        if not sys.stdin.isatty():
            interactive = False
    except:
        interactive = False

    platforms = cl.get_platforms()

    if not platforms:
        raise Error("no platforms found")
    elif len(platforms) == 1 or not interactive:
        platform = 0
    else:
        print "Choose platform from these choices:"
        for i, pf in enumerate(platforms):
            print "[%d] %s" % (i, pf)

        platform = raw_input("Choice [0]: ")
        if not platform:
            platform = 0
        else:
            platform = int(platform)

    devices = platforms[platform].get_devices()

    if not devices:
        raise Error("no devices found")
    elif len(devices) == 1 or not interactive:
        device = 0
    else:
        print "Choose device from these choices:"
        for i, dev in enumerate(devices):
            print "[%d] %s" % (i, dev)

        device = raw_input("Choice [0]: ")
        if not device:
            device = 0
        else:
            device = int(device)

    return (platform, device)

def get_device_somehow(interactive=True):
    return get_device(get_device_indices_somehow(interactive))

def get_device_code_somehow(interactive=True, import_name='ahh.util.opencl'):
    """Returns initialization code for a device either interactively
	(interactive==True) or heuristically.

	Provide the name of the pyopencl library as the the cl_lib_name argument
	if it is not 'pyopencl'.
	"""
    (platform_idx, device_idx) = get_device_indices_somehow(interactive)
    if import_name is None:
        call_name = ""
    else:
        call_name = import_name + "."
    call_name += 'get_device'

    return "device = %s((%d, %d))" % (import_name, platform_idx, device_idx)

## Simple wrapper API
class SimpleDeviceContext(cl.Context):
    """Provides a wrapper of pyopencl.Context exposing some simplifications.

    Simplifications:
     - Only bound to a single device. Multiple devices which are not identical
       can't effectively be used anyway, and multiple identical devices could
       theoretically be supported by the same API (future work.)
     - Specifies a primary command queue which is used by default (self.queue)
     - Simple compile() function.
     - Simpler memory management API for most things
         Methods: alloc, alloc_like, release, memcpy_ptod, memcpy_dtop,
                  to_device, from_device, from_device_like, In, Out, InOut

    Additional capabilities:
     - If save_buffers == True (in constructor), all buffers are
       saved. You can access these with:

         self.buffers is a list of all buffers.

         self.tagged_buffers tags to lists of buffers with that tag, and
           also maps buffers back to their tag.
           (each buffer can only have one tag, the default tag is "Unknown")

       NOTE: This means that automatic garbage collection will not happen until
       the context is deleted.

       If you create a buffer independently and want to register it, call
       _save_buffer(buffer, tag).
	"""
    def __init__(self, device, save_buffers=True):
        cl.Context.__init__(self, [device])
        self._device = device
        self._queue = None
        self._save_buffers = save_buffers
        if save_buffers:
            self._buffers = [ ]
            self._tagged_buffers = { }

    @classmethod
    def get_code_somehow(cls, interactive=True, import_name="ahh.util.opencl",
                         save_buffers=True,):
        """Generates code somehow for SimpleDeviceContext initialization.

        If you want the device selected to always be selected.
		"""
        return "%(device_code)s\ncontext = %(import_name)s.SimpleDeviceContext(device, %(save_buffers)s)" % {
            'device_code': get_device_code_somehow(interactive, import_name),
            'import_name': import_name, 'save_buffers': str(save_buffers)}

    @classmethod
    def get_somehow(cls, interactive=True, save_buffers=True):
        """Creates an instance of SimpleDeviceContext somehow."""
        device = get_device(get_device_indices_somehow(interactive))
        return cls(device, save_buffers)

    @property
    def device(self):
        """Returns the device bound to this context."""
        return self._device
    if 0: isinstance(device, cl.Device)

    @property
    def queue(self):
        """Provides a 'primary' event queue associated with the device."""
        if self._queue is None:
            self._queue = cl.CommandQueue(self)

        return self._queue
    if 0: isinstance(queue, cl.CommandQueue)

    @property
    def save_buffers(self):
        return self._save_buffers
    if 0: isinstance(save_buffers, bool)

    @property
    def buffers(self):
        if self.save_buffers:
            return self._buffers
        else: raise Exception("Not saving buffers.")
    if 0: isinstance(buffers, list)

    @property
    def tagged_buffers(self):
        if self.save_buffers:
            return self._tagged_buffers
        else: raise Exception("Not saving buffers.")
    if 0: isinstance(buffers, dict)

    def _save_buffer(self, buffer, tag="Unknown"):
        if self.save_buffers:
            self._buffers.append(buffer)

            if self._tagged_buffers.has_key(tag):
                self._tagged_buffers[tag].append(buffer)
            else:
                self._tagged_buffers[tag] = [ buffer ]
            self._tagged_buffers[buffer] = tag

    ## Memory management
    def alloc(self, cl_type, length, tag="Unknown"):
        """Allocates a device buffer."""
        buffer = SimpleTypedBuffer(self, cl.mem_flags.WRITE_ONLY, cl_type,
                                   length)
        self._save_buffer(buffer, tag)
        return buffer

    def alloc_like(self, buffer, tag="Unknown"):
        """Allocates a device buffer large enough to hold the specified Python buffer."""
        return self.alloc(to_cl_type[buffer.dtype], len(buffer), tag)

    def release(self, buffer):
        """Frees the memory associated with the provided device buffer."""
        buffer.release()

    def release_all_and_clear(self):
        """Frees the memory associated with all saved buffers and clears the
		list of saved buffers."""
        if (self.save_buffers):
            for buffer in self._buffers:
                self.release(buffer)
            self._buffers.clear()
            self._tagged_buffers.clear()

    def memcpy_ptod(self, dest, src, wait=True):
        """Copies the source Python buffer to the destination device buffer.

        If wait is True (default), does not return until transfer completes.
        If wait is False, returns immediately.

        (OpenCL) Returns the Event instance.
        """
        event = cl.enqueue_write_buffer(self.queue, dest, src,
                                        is_blocking=wait)
        if wait: event.wait()
        return event

    def memcpy_dtop(self, src, dest, wait=True):
        """Copies the source device buffer to the destination Python buffer.

        If wait is True (default), does not return until transfer completes.
        If wait is False, returns immediately.

        (OpenCL) Returns the Event instance.
        """
        event = cl.enqueue_read_buffer(self.queue, dest, src,
                                       is_blocking=wait)
        if wait: event.wait()
        return event

    def to_device(self, src, tag="Unknown"):
        """Copies the source Python buffer to a new device buffer synchronously."""
        buffer = SimpleTypedBuffer(self,
                                   cl.mem_flags.READ_WRITE |
                                   cl.mem_flags.COPY_HOST_PTR,
                                   to_cl_type[src.dtype], len(src), hostbuf=src)
        self._save_buffer(buffer, tag)
        return buffer

    def from_device(self, shape, dtype, src):
        """Copies the source device Buffer to a new Python buffer synchronously."""
        dest = numpy.empty(shape, dtype)
        self.memcpy_dtop(dest, src, True)
        return dest

    def from_device_like(self, like, src):
        """Copies the source device buffer to a new Python buffer synchronously,
        with dimensions taken from the provided Python buffer."""
        return self.from_device(like.shape, like.dtype, src)

    def In(self, buffer, tag="Unknown"):
        """Copies the provided Python buffer to the device. Same as to_device."""
        return self.to_device(buffer, tag)

    def Out(self, buffer, tag="Unknown"):
        """Reads the contents of the uninitialized device buffer into the provided
		host buffer after kernel invocation."""
        buffer = _Out(self, buffer)
        self._save_buffer(buffer, tag)
        return buffer

    def InOut(self, buffer, tag="Unknown"):
        """Copies the provided host buffer to the device and reads the contents
		back into the buffer after kernel invocation."""
        buffer = _InOut(self, buffer)
        self._save_buffer(buffer, tag)
        return buffer

    ## Compiling code
    def compile(self, code):
        """Returns a SimpleProgram built from source."""
        return SimpleProgram(self, code).compile()

class SimpleTypedBuffer(cl.Buffer):
    """Subclass of pyopencl.Buffer with an associated ScalarType and length."""
    def __init__(self, context, flags, cl_type=None, length=None, hostbuf=None):
        if cl_type is None:
            if hostbuf is None:
                raise Exception("No cl_type or hostbuf specified.")
            else:
                cl_type = hostbuf.dtype.cl_type

        if length is None:
            if hostbuf is None:
                raise Exception("No length or hostbuf specified.")
            else:
                length = len(hostbuf)

        self.cl_type = cl_type
        self.length = length

        size = cl_type.size_bytes * length
        cl.Buffer.__init__(self, context, flags, size, hostbuf)

        # print "Initialized buffer (cl_type=%s, length=%d)." % (cl_type.name, length)

class _ArgumentHandler(SimpleTypedBuffer):
    def __init__(self, context, buffer):
        self.context = context
        self.buffer = buffer

class _Out(_ArgumentHandler):
    def __init__(self, context, buffer):
        _ArgumentHandler.__init__(self, context, buffer)
        SimpleTypedBuffer.__init__(self, context, cl.mem_flags.WRITE_ONLY,
                                   to_cl_type[buffer.dtype], len(buffer))

    def _handle_argument(self):
        cl.enqueue_read_buffer(self.context.queue, self, self.buffer,
                               is_blocking=True).wait()

class _InOut(_Out):
    def __init__(self, context, buffer):
        _ArgumentHandler.__init__(self, context, buffer)
        SimpleTypedBuffer.__init__(self, context,
                           cl.mem_flags.READ_WRITE | cl.mem_flags.COPY_HOST_PTR,
                           hostbuf=buffer)

class SimpleProgram(cl.Program):
    """Subclass of pyopencl.Program associated with a SimpleDeviceContext.

	Same API as pyopencl.Program, but produces SimpleKernels instead of Kernels.

    By default, looks up the main queue in the context. If you want to
    override this, set the _queue attribute.
	"""
    def __init__(self, context, src, queue=None):
        cl.Program.__init__(self, context, src)
        self.context = context
        self._queue = queue
        self.kernels = { }

    def __getattr__(self, name):
        if self.kernels.has_key(name):
            return self.kernels[name]
        self.kernels[name] = SimpleKernel(self, name)
        return self.kernels[name]

    @property
    def queue(self):
        if self._queue is None:
            return self.context.queue
        return self._queue

class SimpleKernel(cl.Kernel):
    """Subclass of pyopencl.Kernel associated with a SimpleProgram.

    Same API as pyopencl.Kernel, except it does not require a queue to run,
    instead looking up the queue to use via the SimpleProgram (which in turn
    looks it up in the SimpleDeviceContext, which provides a default queue).

	If you want to override this default behavior, set the _queue attribute
	either here or in the associated SimpleProgram.
	"""
    def __init__(self, program, name, queue=None):
        cl.Kernel.__init__(self, program, name)
        self.program = program
        self._queue = queue

    def __call__(self, global_size, *args, **kwargs):
        cl.Kernel.__call__(self, self.queue, global_size, *args, **kwargs)

        # Support for Out and InOut
        for arg in args:
            if hasattr(arg, '_handle_argument'):
                arg._handle_argument()

    @property
    def queue(self):
        if self._queue is None:
            return self.program.queue
        return self._queue


## Versions
OpenCL_1_0 = ahh.util.Version("OpenCL", [("major", 1), ("minor", 0)])

## Extension descriptors
class Extension(object):
    """OpenCL Extension Descriptor."""
    def __init__(self, name):
        self.name = name

    def get_pragma_str(self, enable=True):
        if enable: enable_str = "enable"
        else: enable_str = "disable"

        return "#pragma extension %s : %s" % (self.name, enable_str)

cl_khr_fp64 = Extension("cl_khr_fp64")
cl_khr_global_int32_base_atomics = Extension("cl_khr_global_int32_base_atomics")
cl_khr_global_int32_extended_atomics = Extension("cl_khr_global_int32_extended_atomics")
cl_khr_local_int32_base_atomics = Extension("cl_khr_local_int32_base_atomics")
cl_khr_local_int32_extended_atomics = Extension("cl_khr_local_int32_extended_atomics")
cl_khr_byte_addressable_store = Extension("cl_khr_byte_addressable_store")

cl_APPLE_gl_sharing = Extension("cl_APPLE_gl_sharing")
cl_APPLE_SetMemObjectDestructor = Extension("cl_APPLE_SetMemObjectDestructor")
cl_APPLE_ContextLoggingFunctions = Extension("cl_APPLE_ContextLoggingFunctions")

int32_atomics_extensions = (cl_khr_global_int32_base_atomics,
                            cl_khr_global_int32_extended_atomics,
                            cl_khr_local_int32_base_atomics,
                            cl_khr_local_int32_extended_atomics)

## OpenCL data type descriptors
class Type(object):
    """OpenCL data type descriptor.

    name
        The standard name of the type.

    synonyms
        Synonymous names for the type.

    version
        The first OpenCL version the type is available in.

    required_extension
        If an extension is required, this provides a descriptor for it.

    tags
        A set of string tags for this type.
    """
    def __init__(self, name, synonyms, version, required_extension, tags):
        self.name = name
        if 0: isinstance(self.name, str)

        self.synonyms = synonyms
        if 0: isinstance(self.synonyms, list)

        self.version = version
        if 0: isinstance(self.version, ahh.util.Version)

        self.required_extension = required_extension
        if 0: isinstance(self.required_extension, Extension)

        self.tags = tags
        if 0: isinstance(self.tags, set)

    @property
    def expression(self):
        return self.name

## = Scalar type descriptors
class ScalarType(Type):
    """OpenCL fixed-size scalar data type descriptor.

    In addition to attributes defined by Type, the following additional
    attributes are available:

    dtype
        numpy datatype corresponding to this scalar type, or None if unsupported by numpy.

    size_bytes
        The size, in bytes, of this data type.

    literal_suffix
        The suffix appended to literals for this datatype, or None.
        (e.g. 'f' for float)

        Note that either case can normally be used. The preferred case is
        supplied here.

        Raw integer and floating point literals default to int and double,
        respectively, unless the integer exceeds the bounds for 32-bit integers
        in which case it is promoted to a long. (I think.)
    """
    def __init__(self, name, synonyms, version, required_extension, dtype,
                 size_bytes, literal_suffix, tags):
        Type.__init__(self, name, synonyms, version, required_extension, tags)

        self.dtype = dtype
        if 0: isinstance(self.dtype, numpy.dtype)

        self.size_bytes = size_bytes
        if 0: isinstance(self.size_bytes, int)

        self.literal_suffix = literal_suffix
        if 0: isinstance(self.literal_suffix, str)

    def make_literal(self, bare_literal):
        """Converts a bare literal into an appropriately typed literal.

        Adds a suffix, if one exists. If not, uses a cast.
        """
        literal_suffix = self.literal_suffix
        if literal_suffix is None:
            return "(%s)%s" % (self.name, bare_literal)
        else:
            return "%s%s" % (bare_literal, literal_suffix)

class PointerType(Type):
    def __init__(self, parent):
        Type.__init__(self, parent.name + "*", ( synonym + "*" for synonym in
            parent.synonyms), parent.version, parent.required_extension,
            ("pointer"))

## == Define singletons for standard scalar types
scalar_types = set()
"""Set of all OpenCL fixed-size scalar data type descriptors."""

to_dtype = { }
to_cl_type = { }

def _define_scalar_type(name, synonyms, version, required_extension, dtype_name, size_bytes, literal_suffix, tags):
    if dtype_name is not None:
        try:
            dtype = numpy.dtype(dtype_name)
            assert dtype.itemsize == size_bytes, \
             "Numpy data type and OpenCL scalar type sizes must be consistent."
        except TypeError:
            dtype = None
    else:
        dtype = None

    cl_type = ScalarType(name, synonyms, version, required_extension,
                         dtype, size_bytes, literal_suffix, tags)
    scalar_types.add(cl_type)

    if dtype is not None:
        cl_type.tags.add("numpy-supported")
        to_dtype[cl_type] = dtype
        to_cl_type[dtype] = cl_type
        if hasattr(numpy, dtype_name):
            # numpy.int32 is not numpy.dtype(numpy.int32)
            # but can often be used interchangeably
            to_cl_type[getattr(numpy, dtype_name)] = cl_type

    return cl_type

cl_char = _define_scalar_type("char", [],
                              OpenCL_1_0, None,
                              "int8", 1,
                              None, set(("integer", "signed")))
cl_uchar = _define_scalar_type("uchar", ["unsigned char"],
                               OpenCL_1_0, None,
                               "uint8", 1,
                               None, set(("integer", "unsigned")))
cl_short = _define_scalar_type("short", [],
                               OpenCL_1_0, None,
                               "int16", 2,
                               None, set(("integer", "signed")))
cl_ushort = _define_scalar_type("ushort", ["unsigned short"],
                                OpenCL_1_0, None,
                                "uint16", 2,
                                None, set(("integer", "unsigned")))
cl_int = _define_scalar_type("int", [],
                             OpenCL_1_0, None,
                             "int32", 4,
                             None, set(("integer", "signed")))

cl_int.make_literal = lambda(literal): str(int(literal))  # override default behavior

cl_uint = _define_scalar_type("uint", ["unsigned int"],
                              OpenCL_1_0, None,
                              "uint32", 4,
                              None, set(("integer", "unsigned")))
cl_long = _define_scalar_type("long", [],
                              OpenCL_1_0, None,
                              "int64", 8,
                              "L", set(("integer", "signed")))
cl_ulong = _define_scalar_type("ulong", ["unsigned long"],
                               OpenCL_1_0, None,
                               "uint64", 8,
                               "uL", set(("integer", "unsigned")))

cl_float = _define_scalar_type("float", [],
                               OpenCL_1_0, None,
                               "float32", 4,
                               "f", set(("float")))
cl_double = _define_scalar_type("double", [],
                                OpenCL_1_0, cl_khr_fp64,
                                "float64", 8,
                                "", set(("float")))

cl_double.make_literal = lambda(literal): str(float(literal))

# TODO: half has special semantics, not including here because I don't quite
# understand how it works and also is not supported by numpy right now.

# TODO: Scalar types with machine-dependent (device-dependent?) size
# bool
# size_t
# ptrdiff_t
# intptr_t
# uintptr_t
# void(?)

# Reserved as of OpenCL 1.0 and supported by numpy but not implemented by OpenCL
#"quad": numpy.float128,
#"complex float": numpy.complex64,
#"imaginary float": numpy.complex64,
#"complex double": numpy.complex128,
#"imaginary double": numpy.complex128,
#"complex quad": numpy.complex256,
#"imaginary quad": numpy.complex256,
#"long long": numpy.int128,
#"ulong long": numpy.uint128,
#"unsigned long long": numpy.uint128

# Reserved as of OpenCL 1.0 but not supported by numpy
#complex half
#imaginary half

# Reserved as of OpenCL 1.0, not sure what this is
# long double
# floatnxm
# doublenxm

# Other built-in data types
# image2d_t
# image3d_t
# sampler_t
# event_t

## == Literal conversion for scalar types
def to_cl_numeric_literal(value, report_type=False,
                          force_int32_max=False, force_float32_max=False,
                          unsigned=False):
    """Produces an OpenCL numeric literal from a number-like value.

    report_type
        If True, returns (OpenCL type descriptor, literal).
    	If False, returns the literal.

    force_int32_max, force_float32_max
        If True, 64-bit and higher integers/floats produce 32-bit literals
    	*with no bounds checking* unless you use the explicit numpy type.

    unsigned
        If True, returns unsigned variant for integers.

    See source for full algorithm.

    Examples:
    >>> to_cl_numeric_literal(4)
    "4"

    >>> to_cl_numeric_literal(4.0)
    "4.0"

    >>> to_cl_numeric_literal(4.0, force_float32_max=True)
    "4.0f"

    >>> to_cl_numeric_literal("4.0", force_float32_max=True)
    "4.0f"

    >>> to_cl_numeric_literal("4", force_int32_max=True, report_type=True)
    (<ahh.util.opencl.cl_int>, "(int)4")

    >>> to_cl_numeric_literal(4.0, report_type=True)
    (<ahh.util.opencl.cl_double>, "4.0")

    >>> to_cl_numeric_literal("abc")
    None

    See also: Type.make_literal to specify the type explicitly.
    Bugs: Issue 22
    """

    str_rep = str(value)

    ## Figure out type
    is_numpy = hasattr(value, "dtype") and value.dtype in to_cl_type
    if is_numpy:
        cl_type = to_cl_type[value.dtype]
    else:
        # TODO: Bounds checking.
        if ahh.util.np.int_size_bits == 32 or force_int32_max:
            if unsigned:
                int_type = cl_uint
            else:
                int_type = cl_int
        else:
            if unsigned:
                int_type = cl_ulong
            else:
                int_type = cl_long

        if isinstance(value, int) or isinstance(value, long):
            cl_type = int_type
        else:
            # TODO: Bounds checking.
            if ahh.util.np.float_size_bits == 32 or force_float32_max:
                float_type = cl_float
            else:
                float_type = cl_double

            if isinstance(value, float):
                cl_type = float_type
            else:
                if ahh.util.is_long_like(value):
                    cl_type = int_type
                elif ahh.util.is_float_like(value):
                    cl_type = float_type
                else:
                    return None


    ## Add appropriate suffix / cast
    cl_literal = cl_type.make_literal(str_rep)

    ## Return value
    if report_type:
        return (cl_type, cl_literal)
    else:
        return cl_literal

## Code gen
class GenerationDetails(util.codegen.ExpressionDetails):
    def __init__(self, parent, param_name, default_hook, cl_type):
        util.codegen.ExpressionDetails.__init__(self, parent, param_name,
                                                default_hook)
        self.cl_type = cl_type

    def make_literal(self, literal):
        return self.cl_type.make_literal(literal)

## = Strings
def to_cl_string_literal(value):
    """
    Produces an OpenCL string literal from a string value.

    >>> to_cl_string_literal("4.0")
    '"4.0"'

    >>> to_cl_string_literal("abc\n")
    '"abc\\n"'
    """
    if isinstance(value, str):
        return '"%s"' % ahh.util.string_escape(value)
    else:
        return None

##
if __name__ == "__main__":
    print get_device_code_somehow()

## Experimental
CUDA_Equivalents = {
    '__kernel': '__device__',
    '__global': '__global__'
}
